Next(.js) on Page and the "Your Worker exceeded the size limit of XX MB" Issue

Problem Solving
Published 2024-12-20
POST

For whatever reason, wanting to deploy Next.JS to Cloudflare Pages and use Server-side Rendering (SSR) led me to use next-on-pages which runs on Cloudflare Page Function (wrapper Cloudflare Worker)

If you're only interested in the solution, jump to the end

Problem

When we add features to our website up to a certain point, it's not unusual to hit limitations or errors like today. If we interpret the log we see, the Function file we built when running pnpm next-on-pages && wrangler pages deploy exceeds 1 MiB in size (currently 3 MiB for Free Tier)

Now that we know the problem, what's the best way to fix it??

Process to Find Solution

Finding Reference Points

When we encounter new problems, it's like being in the dark. Where do we start?

Personally, I recommend finding reference points first, comparing with the state before the problem occurred.

In this case, we look at when was the last successful build.

Looking for Changes

Once we know the turning point that started causing build failures, let's find the root cause of the error.

The first thing to try is to checkout both commits and build them, comparing the build outputs. When working with Next.JS, we typically look at results from vercel build

bash | before.log
1
...
2
✓ Generating static pages (5/5)
3
Finalizing page optimization ...
4
Collecting build traces ...
5
6
Route (app) Size First Load JS
7
┌ ƒ / 36.8 kB 138 kB
8
...
9
├ ƒ /error 1.08 kB 102 kB
10
├ ○ /icon.svg 0 B 0 B
11
├ ○ /privacy-policy 1.07 kB 88.2 kB
12
├ ƒ /signin 1.08 kB 102 kB
13
├ ○ /terms-of-service 1.07 kB 88.2 kB
14
└ ƒ /workbench 7.93 kB 109 kB
15
+ First Load JS shared by all 87.2 kB
16
├ chunks/376-ae8867d1f8dbbcbb.js 31.5 kB
17
├ chunks/f14ca715-3ecd66d7a69888bb.js 53.6 kB
18
└ other shared chunks (total) 1.98 kB
19
20
21
ƒ Middleware 103 kB
22
(Static) prerendered as static content
23
ƒ (Dynamic) server-rendered on demand
24
...
bash | after.log
1
...
2
✓ Generating static pages (5/5)
3
Finalizing page optimization ...
4
Collecting build traces ...
5
6
Route (app) Size First Load JS
7
┌ ƒ / 36.8 kB 138 kB
8
...
9
├ ƒ /error 1.09 kB 102 kB
10
├ ○ /icon.svg 0 B 0 B
11
├ ƒ /payment-success 1.09 kB 102 kB
12
├ ○ /privacy-policy 1.07 kB 88.5 kB
13
├ ƒ /signin 1.09 kB 102 kB
14
├ ○ /terms-of-service 1.07 kB 88.5 kB
15
└ ƒ /workbench 95.4 kB 196 kB
16
+ First Load JS shared by all 87.4 kB
17
├ chunks/376-8534b4cf2341312a.js 31.7 kB
18
├ chunks/f14ca715-5320c06222168bec.js 53.6 kB
19
└ other shared chunks (total) 2.04 kB
20
21
22
ƒ Middleware 103 kB
23
(Static) prerendered as static content
24
ƒ (Dynamic) server-rendered on demand
25
...

When we see the file size jump from 453.30 KiB -> 2449.81 KiB (~2.4 MiB), it becomes clear where the real problem lies. It's definitely the __next-on-pages-dist__/functions/workbench.func.js file exceeding 1 MB.

An interesting observation about how Worker Size Limit rules are set - whether they look at total file size or individual files. When we check Routing or Limit documentation, nothing specifies which size to count. But looking at the numbers above, whether before or after the fix 2363.35 KiB vs 4450.53 KiB, both exceed 1 MB (Current 3 MB), suggesting they count individual file sizes.

Once we know which file is oversized, we trace back to see which lines were added by looking at the Diff (Difference) between these 2 commits 1b6bf08 and cde7e3a

compare commit

This is a good example of why we should learn how to use Git and understand what makes a good commit, how to name commits, how big a commit should be, when to separate commits, when to squash and merge, or when to use merge commits. If we choose the right method, the result should be a clear history that communicates what happened to the code.

We then look at which files are related to src/app/workbench/page.tsx by checking imports or nested imports that have files in these 2 commit diffs.

In this case, it's the AIBlock.tsx file that added a Markdown Parser, and this parser is larger than 1 MB, causing workbench.func.js to exceed the size limit.

A quick way to check how much size a feature adds is to build and compare between feature-off and feature-on states. If you can't think of how to turn off a feature, you can simply comment out the lines using that feature.

Handling the Problem

Now that we know the root cause, we need to choose how to handle this problem. We need to consider if this feature is important for SSR, and if there are any cases where we need to render Markdown before it reaches the client.

In this case, we don't use Markdown rendering on the SSR side, so we can choose to lazy-load this React Component.

ts | page.tsx
1
2
-
import { AIBlock } from "./AIBlock.tsx";
3
+
import { lazy } from "react";
4
+
const AIBlock = lazy(() =>
5
+
import("./AIBlock.tsx").then((m) => ({ default: m.AIBlock }))
6
+
);
7
// ...Function Render Component...

Let's test after the fix.

The result from vercel build shows smaller size, but wrangler pages functions build --build-output-directory .vercel/output/static gives almost the same size.

bash | before.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 2449.81 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤
bash | after.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 2450.28 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤

Before vs After -> 2449.81 KiB vs 2450.28 KiB isn't very satisfying. When we see that the result isn't what we expected, it's a good time to check the Next.js Lazy Loading Documentation

If we only read this far, we might interpret that React.lazy() gives the same result as next/dynamic. But if we scroll down to the Skipping SSR example:

We'll find that although React.lazy() does lazy load on the client, it still pre-renders on the server, which is why workbench.func.js still needs to include the Markdown render feature, resulting in no size reduction.

Solution

Finally, we reach the real solution. Simply change from React.lazy() to next/dynamic with the { ssr: false } option.

ts | page.tsx
1
-
import { lazy } from "react";
2
-
const AIBlock = lazy(() =>
3
-
import("./AIBlock").then((m) => ({ default: m.AIBlock }))
4
-
);
5
+
import dynamic from "next/dynamic"
6
+
const AIBlock = dynamic(() =>
7
+
import("./AIBlock").then((m) => ({ default: m.AIBlock })),
8
+
{ ssr: false }
9
+
);
10
// ...Function Render Component...

And when built:

bash | before.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 2449.81 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤
bash | after.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 455.59 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤

Size is now under 1 MB and Deploy Success! Yay!

Happy Very Funny GIF by Disney Zootopia

Caution: When using React.lazy() or next/dynamic, don't forget to consider the timing of when the Component needs to lazy-load. These methods will cause UI jank, which might be frustrating for users.

The solution is to use Suspense or loading options to show a fallback while the Component is loading.

Summary

When we face problems that even Stackoverflow can't solve, it doesn't mean there's no solution. By systematically looking for solutions, starting with narrowing down the scope until finding the root cause, then looking for fixes. If direct searches don't help, we need to study our tools ourselves, starting with reading docs as the first recommendation. If that's not enough, we can check the Source Code if it's Open Source. If not, we might need to reverse engineer or switch to different tools. Eventually, we'll find a suitable solution.

Will share more interesting problems next time, see you in the next post~